#! /usr/bin/python

'''

This file is a String wrapper which treats them as SExpressions. This
integrates several useful abilities. It accepts as input a string
representation of an SExpression, removes the syntatic sugar that this
program accepts, and then provides a nice interface for usage. 


@author kllvql
@date 12/24/11

'''

from ProjectFlags import *

TEST  = False
DEBUG = False

TEST  = TEST  or TEST_ALL
DEBUG = DEBUG or DEBUG_ALL



#This code assumes these are all singular characters. I could change it
#but I think that's a safe assumption to make, I mean these constants
#are a little excessive already. 
_START 	= '('
_END 	= ')'
_DELIM	= ' '
_STRING = '"'
_COM	= '%'
_QUOTE	= "'"
_EOL	= '\n'
_FWINEOL= '\r\n'
_ODDEOL = '\r'
_RODDEOL= '\n\r'
_ESC	= '\\'

class SExpression:
	
	@staticmethod
	def loadSExpression(filename):
		'''
		Loads an SExpression as from a file. So, this is how you do
		static methods in Python...
		'''
		with open(filename, 'r') as thisFile:
			return SExpression(thisFile.read())
	
	def __init__(self, sExp, sweetened=True):
		'''
		Builds a SExpression off of a string representation of an 
		sExpression. It assumes the sExp has syntatic sugar, so it
		bitters it, but if it is unsweetened, it will skip this step,
		saving processing time.
		'''
		self.rawSExp = sExp
		
		if sweetened:
			self._bitter()
		else:
			self.sExp = sExp
		
	def __str__(self):
		return self.sExp
		
	def _bitter(self):
		'''
		This function removes syntatic sugar from an SExpression, 
		basically cutting all whitespace down to a single space, 
		removing comments, changing 'a to (quote a), etc. The results
		are stored in self.sExp
		'''
		self.sExp = self.rawSExp
		
		self._fixEOL()
		self._removeComments()
		self._removeWhitespace()
		#self._backquote()
		self._quote()
		return self
	
	def _fixEOL(self):
		'''
		Replaces '\r\n' '\r' and '\n\r' with simply '\n'. This is
		just in case people on odd OS's like Windows choose to run
		my software.
		'''
		sExp = self.sExp
		
		sExp = sExp.replace(_FWINEOL,_EOL)
		sExp = sExp.replace(_ODDEOL ,_EOL)
		sExp = sExp.replace(_RODDEOL,_EOL)
		
		self.sExp = sExp
		return self
	
	def _removeComments(self):
		'''
		Removes comments from the SExpression. Comments begin with % 
		and end with \n. (After the various types of EOL have been 
		handled)
		'''
		sExp = self.sExp
		comment = sExp.find(_COM)
		while comment != -1:
			nextLine = sExp.find(_EOL, comment)
			sExp = sExp[:comment] + sExp[nextLine+1:]
			comment = sExp.find(_COM)
		self.sExp = sExp
		return self
	
	def _removeWhitespace(self):
		'''
		Removes whitespace characters from the code and replaces them
		with the space character. Then extraneous spaces are removed.
		Whitespace recognized is '\t','\n','\v', and '\f'
		'''
		sExp = self.sExp
		
		# Replace tabs and newlines with delim
		whitespace = ['\t','\n','\v','\f']
		for white in whitespace:
			sExp = sExp.replace(white,_DELIM)
		
		i = 0
		while i < (len(sExp)-1):
			# Get convert multiple spaces to single space
			if sExp[i] == _DELIM and sExp[i+1] == _DELIM:
				sExp = sExp[:i] + sExp[i+1:]
				i -= 1
			i = i + 1
		
		# Removes first character if it is delim
		if sExp[0] == _DELIM:
			sExp = sExp[1:]
		# Removes last character if it is delim
		if sExp[len(sExp)-1] == _DELIM:
			sExp = sExp[:-1]
		# Remove character before ')' if it's delim and char
		# after '(' if it's delim
		i = 0
		while i < (len(sExp)-1):
			if sExp[i] == _DELIM and sExp[i+1] == _END:
				sExp = sExp[:i] + sExp[i+1:]
			elif sExp[i] == _START and sExp[i+1] == _DELIM:
				sExp = sExp[:i+1] + sExp[i+2:]
			i += 1
	
		
		self.sExp = sExp
		return self
	
	def _quote(self):
		'''
		This method takes self.sExp and looks at any  locations where
		the _QUOTE character is immediately followed by the _START 
		character. It then de-sugars it into the (quote ...) form
		'''
		sExp = self.sExp
		
		i = 0
		while i < len(sExp):
			quote = sExp.find(_QUOTE,i)
			if quote == -1: break
			if sExp[quote+1] == _START:
				end = self._match(quote+1,sExp)
				sExp = sExp[0:quote] + '(quote ' + sExp[quote+1:end] + \
					   ')' + sExp[end:]
			i += 1
		self.sExp = sExp
		return self
	
	def _match(self, i, string=None):
		'''
		Finds the token which starts at i and returns the end of it. If 
		i is a _START, it finds the matching _END. Otherwise, it returns
		the index of the next _DELIM.
		
		eg. (one (two))._match(0) = 11 and ._match(1) = 4
		'''
		string = self.sExp if string == None else string
		if string[i] != _START:
			if string[i] != _STRING:
				return string.find(_DELIM, i)
			else:
				endStr = self._stringMatch(string, i)
				return string.find(_STRING, endStr)
		else:
			start, end, j = 1,0,i+1
			while start != end:
				if j >= len(string)-1:
					print start, end, string
				char = string[j]
				if char == _START:
					start += 1
				elif char == _END:
					end += 1
				j += 1
			return j
	
	@staticmethod
	def _stringMatch(string, i):
		'''
		Accepts a string, and an index which is the location of a 
		_STRING char which you wish to match. Returns an index of 
		string which is one more than the matching _STRING char for 
		i. string[i:_stringMatch(string,i)] would return the quoted
		string you are searching for, with quotes included. Returns
		-1 if not found. 
		'''
		if i >= len(string)-1:
			return -1 
		for j in range(i+1,len(string)):
			if string[j] == _STRING and string[j-1] != _ESC:
				return j
		else:
			return -1
	
	def car(self):
		'''
		Returns the car of the list. The returned is a SExpression, but
		it may not return true for the isList() method. If this is the 
		case, it is an identifier, not a token.
		'''
		end = self._match(1)
		newSExp = self.sExp[1:end]
		if newSExp[0] == _STRING: 
			newSExp = newSExp[1:]
	
		return SExpression(newSExp, False)

	def cdr(self):
		'''
		Returns the cdr of the list, as long as the list is non-empty. 
		if it is empty, it returns the empty list
		'''
		end = self._match(1)
		if end != -1:		
			return SExpression(_START + self.sExp[end+1:-1] + _END, \
							   False)
		else:
			return SExpression(_START + _END, False)

	def caar(self):
		'''
		Returns self.car().car()
		The first item in the first list in the list.
		'''
		return self.car().car()
	
	def cadr(self):
		'''
		returns self.car().cdr()
		The rest of the first of the list.
		'''
		return self.car().cdr()
	
	def cddr(self):
		'''
		Returns self.cdr().cdr()
		'''
		return self.cdr().cdr()
	
	def cdar(self):
		'''
		Returns self.cdr.car()
		'''
		return self.cdr.car()
		

	def isEmpty(self):
		'''
		Returns True if sExpression is the empty list, False otherwise
		'''
		return len(self.sExp) == 2

	def isList(self):
		'''
		Returns True if string is an SExpression, false otherwise. This
		is mainly for the output of car. The output of cdr will always 
		return true for this.
		'''
		return self.sExp[0]==_START and self.sExp[-1]==_END


	def iterate(self, flatten=False):
		'''
		Allows one to iterate over the tokens in an sExpression. 
		If flatten is set to false, the sExpression is iterated over,
		and returns lists and/or atoms as is appropriate for each
		element. If flatten is True, however, this recursively iterates
		over each sub-list, and returns ever atom contained however
		deep in self. 
		'''
		import copy
		sExp = copy.copy(self)
		while not sExp.isEmpty():
			thisCar = sExp.car()
			sExp = sExp.cdr()
			if flatten:
				if thisCar.isList():
					for token in thisCar.iterate(flatten):
						yield token
				else:
					yield thisCar
			else:
				yield thisCar


def test():
	print "SExpression.py testing..."
	#s =  '(Character (name (type string)) (age (type (range (min 10) '
	#s += '(max 20)))) (inventory (type bag)))'
	#print s
	#sExp = SExpression(s)
	#print str(sExp)
	#for elem in sExp.iterate(True):
		#print elem]
		
		
	#sExp = SExpression.loadSExpression('Tests/Character01.s')
	#print(str(sExp))
	
	s = "alallaljkldjflkj"
	print len(s)
	SExpression._stringMatch(s,3)
	
	print "End SExpression.py testing. \n"

if __name__ == '__main__' or TEST:
	test()
